package com.seewo.dubbo.debugger.core;


import com.seewo.dubbo.debugger.api.DataType;
import com.seewo.dubbo.debugger.api.model.ApiDef;
import com.seewo.dubbo.debugger.api.model.Document;
import com.seewo.dubbo.debugger.api.model.ModelDef;
import com.seewo.dubbo.debugger.api.model.ParamDef;
import com.seewo.dubbo.debugger.common.Comment;
import com.seewo.dubbo.debugger.common.CommentParse;
import com.seewo.dubbo.debugger.common.Util;
import org.springframework.context.ApplicationContext;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.core.ParameterNameDiscoverer;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Parameter;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicBoolean;

/**
 * @author chenliangliang
 * @date 2018/7/26
 */
public class ApiContext {

    private static ConcurrentHashMap<String, Document> apiMap = new ConcurrentHashMap<>(16);
    private static ConcurrentHashMap<String, ModelDef> modelMap = new ConcurrentHashMap<>(32);

    private static CommentParse commentParse = CommentParse.loadDefault();
    private static ParameterNameDiscoverer discoverer = new DefaultParameterNameDiscoverer();

    private static ApplicationContext context;
    private static CountDownLatch latch;
    private static AtomicBoolean isLoaded = new AtomicBoolean(false);
    private static Map<String, Document> unmodifiableApiMap;

    static void setLatch(CountDownLatch latch) {
        ApiContext.latch = latch;
    }

    static void setApplicationContext(ApplicationContext ctx) {
        ApiContext.context = ctx;
    }

    public static ApplicationContext getApplicationContext() {
        return context;
    }

    public static Map<String, Document> getApis() {
        //等待结果汇总
        try {
            latch.await();
        } catch (InterruptedException e) {
            //ignore
        }
        if (isLoaded.compareAndSet(false, true)) {
            unmodifiableApiMap = Collections.unmodifiableMap(apiMap);
        }
        return unmodifiableApiMap;
    }

    public static void addApi(Class<?> apiClass) {
        Document document = buildDocument(apiClass);
        Method[] methods = apiClass.getDeclaredMethods();
        List<ApiDef> apiList = new ArrayList<>(16);
        for (Method method : methods) {
            if (!Modifier.isPublic(method.getModifiers())) {
                method.setAccessible(true);
            }
            apiList.add(buildApi(method));
        }
        document.setApiList(apiList);
        apiMap.put(document.getInterfaceName(), document);
    }

    private static ApiDef buildApi(Method method) {
        ApiDef apiDef = new ApiDef();
        apiDef.setMethodName(Util.getMethodName(method));
        Comment comment = null;
        if (commentParse != null) {
            comment = commentParse.getMethodComment(method);
        }
        if (comment != null) {
            apiDef.setDescription(comment.getComment());
        }

        //构建requestParams
        List<ParamDef> requestParamList = buildRequestParam(method, comment);
        apiDef.setRequestParams(requestParamList);

        //构建responseParam
        apiDef.setResponseParam(buildResponseParam(method, comment));
        return apiDef;
    }

    private static ParamDef buildResponseParam(Method method, Comment comment) {
        Class<?> returnType = method.getReturnType();
        ParamDef paramDef = new ParamDef();
        paramDef.setName(returnType.getSimpleName());
        if (comment != null) {
            paramDef.setDescription(comment.getRet());
        }
        setType(paramDef, returnType);
        return paramDef;
    }

    private static List<ParamDef> buildRequestParam(Method method, Comment comment) {
        List<ParamDef> paramDefs = new ArrayList<>(8);
        ParamDef paramDef;
        Parameter[] parameters = method.getParameters();
        String[] parameterNames = getParameterNames(method);
        for (int i = 0, l = parameters.length; i < l; i++) {
            Parameter parameter = parameters[i];
            String paramName = parameterNames[i];

            paramDef = new ParamDef();
            paramDef.setName(paramName);
            paramDef.setIndex(i);
            if (comment != null) {
                paramDef.setDescription(comment.getParam(paramName));
            }
            Class<?> parameterType = parameter.getType();
            setType(paramDef, parameterType);

            paramDefs.add(paramDef);
        }
        return paramDefs;
    }

    private static void setType(ParamDef paramDef, Class<?> parameterType) {
        String type = DataType.getType(parameterType);
        paramDef.setType(type);
        String typeName;
        if (DataType.OBJECT.getValue().equals(type)) {
            typeName = parameterType.getCanonicalName();
            setType0(paramDef, parameterType, typeName);
        } else if (DataType.ARRAY.getValue().equals(type)) {
            typeName = parameterType.getName().substring(2);
            setType0(paramDef, parameterType, typeName);
        }
    }

    private static void setType0(ParamDef paramDef, Class<?> parameterType, String typeName) {
        ModelDef modelDef = modelMap.get(typeName);
        if (modelDef == null) {
            modelDef = addModel(parameterType);
        }
        paramDef.setModelDef(modelDef);
    }

    private static ModelDef addModel(Class<?> modelClass) {
        ModelDef modelDef = new ModelDef();
        modelDef.setClassName(modelClass.getCanonicalName());

        List<ParamDef> paramDefs = new ArrayList<>(16);
        Field[] fields = modelClass.getDeclaredFields();

        ParamDef paramDef;
        for (Field field : fields) {
            int modifiers = field.getModifiers();
            if (Modifier.isStatic(modifiers) || Modifier.isFinal(modifiers)) {
                continue;
            }
            paramDef = new ParamDef();
            paramDef.setName(field.getName());
            paramDef.setDescription("");
            Class<?> fieldType = field.getType();
            setType(paramDef, fieldType);

            paramDefs.add(paramDef);
        }
        modelDef.setPropertyList(paramDefs);
        modelMap.put(modelDef.getClassName(), modelDef);
        return modelDef;
    }

    private static Document buildDocument(Class<?> apiClass) {
        Document document = new Document();
        document.setInterfaceName(apiClass.getCanonicalName());
        if (commentParse != null) {
            String comment = commentParse.getClassComment(apiClass);
            document.setDescription(comment);
        }
        return document;
    }


    private static String[] getParameterNames(Method interfaceMethod) {
        Object bean = context.getBean(interfaceMethod.getDeclaringClass());
        Class<?> beanClass = bean.getClass();
        String[] parameterNames = null;
        try {
            Method method = beanClass.getDeclaredMethod(interfaceMethod.getName(), interfaceMethod.getParameterTypes());
            parameterNames = discoverer.getParameterNames(method);
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        }
        return parameterNames;
    }

}
